Passed
Pull Request — master (#127)
by
unknown
01:33
created

index.js ➔ readSync   F

Complexity

Conditions 28

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 6
rs 0
c 0
b 0
f 0
cc 28

How to fix   Complexity   

Complexity

Complex classes like index.js ➔ readSync often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
const fs = require('fs')
2
const ID3Definitions = require("./src/ID3Definitions")
3
const ID3Frames = require('./src/ID3Frames')
4
const ID3Util = require('./src/ID3Util')
5
const zlib = require('zlib')
6
7
/*
8
**  Used specification: http://id3.org/id3v2.3.0
9
*/
10
11
/**
12
 * @param {any} value
13
 * @returns {boolean} true if value is a function
14
 */
15
const isFunction = (value) => value && typeof value === 'function'
16
17
/**
18
 * @param {any} value
19
 * @returns  {boolean} true if value is a string
20
 */
21
const isString = (value) => typeof value === 'string' || value instanceof String
22
23
/**
24
 * Write passed tags to a file/buffer
25
 * @param tags - Object containing tags to be written
26
 * @param filebuffer - Can contain a filepath string or buffer
27
 * @param fn - (optional) Function for async version
28
 * @returns {boolean|Buffer|Error}
29
 */
30
module.exports.write = function(tags, filebuffer, fn) {
31
    let completeTag = this.create(tags)
32
    if(filebuffer instanceof Buffer) {
33
        filebuffer = this.removeTagsFromBuffer(filebuffer) || filebuffer
34
        let completeBuffer = Buffer.concat([completeTag, filebuffer])
35
        if(isFunction(fn)) {
36
            fn(null, completeBuffer)
37
            return undefined
38
        } else {
0 ignored issues
show
Comprehensibility introduced by
else is not necessary here since all if branches return, consider removing it to reduce nesting and make code more readable.
Loading history...
39
            return completeBuffer
40
        }
41
    }
42
43
    if(isFunction(fn)) {
44
        try {
45
            fs.readFile(filebuffer, function(err, data) {
46
                if(err) {
47
                    fn(err)
48
                    return
49
                }
50
                data = this.removeTagsFromBuffer(data) || data
51
                let rewriteFile = Buffer.concat([completeTag, data])
52
                fs.writeFile(filebuffer, rewriteFile, 'binary', (err) => {
53
                    fn(err)
54
                })
55
            }.bind(this))
56
        } catch(err) {
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
57
            fn(err)
58
        }
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
59
    } else {
60
        try {
61
            let data = fs.readFileSync(filebuffer)
62
            data = this.removeTagsFromBuffer(data) || data
63
            let rewriteFile = Buffer.concat([completeTag, data])
64
            fs.writeFileSync(filebuffer, rewriteFile, 'binary')
65
            return true
66
        } catch(err) {
67
            return err
68
        }
69
    }
70
}
71
72
/**
73
 * Creates a buffer containing the ID3 Tag
74
 * @param tags - Object containing tags to be written
75
 * @param fn fn - (optional) Function for async version
76
 * @returns {Buffer}
77
 */
78
module.exports.create = function(tags, fn) {
79
    let frames = []
80
81
    //  Create & push a header for the ID3-Frame
82
    const header = Buffer.alloc(10)
83
    header.fill(0)
84
    header.write("ID3", 0)              //File identifier
85
    header.writeUInt16BE(0x0300, 3)     //Version 2.3.0  --  03 00
86
    header.writeUInt16BE(0x0000, 5)     //Flags 00
87
88
    //Last 4 bytes are used for header size, but have to be inserted later, because at this point, its size is not clear.
89
    frames.push(header)
90
91
    frames = frames.concat(this.createBuffersFromTags(tags))
92
93
    //  Calculate frame size of ID3 body to insert into header
94
95
    let totalSize = 0
96
    frames.forEach((frame) => {
97
        totalSize += frame.length
98
    })
99
100
    //  Don't count ID3 header itself
101
    totalSize -= 10
102
    //  ID3 header size uses only 7 bits of a byte, bit shift is needed
103
    let size = ID3Util.encodeSize(totalSize)
104
105
    //  Write bytes to ID3 frame header, which is the first frame
106
    frames[0].writeUInt8(size[0], 6)
107
    frames[0].writeUInt8(size[1], 7)
108
    frames[0].writeUInt8(size[2], 8)
109
    frames[0].writeUInt8(size[3], 9)
110
111
    if(isFunction(fn)) {
112
        fn(Buffer.concat(frames))
113
    } else {
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
114
        return Buffer.concat(frames)
115
    }
116
}
117
118
/**
119
 * Returns array of buffers created by tags specified in the tags argument
120
 * @param tags - Object containing tags to be written
121
 * @returns {Array}
122
 */
123
module.exports.createBuffersFromTags = function(tags) {
124
    let frames = []
125
    if(!tags) {
126
        return frames
127
    }
128
    const rawObject = Object.keys(tags).reduce((acc, val) => {
129
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
130
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
131
        } else {
132
            acc[val] = tags[val]
133
        }
134
        return acc
135
    }, {})
136
137
    Object.keys(rawObject).forEach((specName) => {
138
        let frame
139
        // Check if invalid specName
140
        if(specName.length !== 4) {
141
            return
142
        }
143
        if(ID3Frames[specName] !== undefined) {
144
            frame = ID3Frames[specName].create(rawObject[specName], 3, this)
145
        } else if(specName.startsWith('T')) {
146
            frame = ID3Frames.GENERIC_TEXT.create(specName, rawObject[specName], 3)
147
        } else if(specName.startsWith('W')) {
148
            if(ID3Util.getSpecOptions(specName, 3).multiple && rawObject[specName] instanceof Array && rawObject[specName].length > 0) {
149
                frame = Buffer.alloc(0)
150
                // deduplicate array
151
                for(let url of [...new Set(rawObject[specName])]) {
152
                    frame = Buffer.concat([frame, ID3Frames.GENERIC_URL.create(specName, url, 3)])
153
                }
154
            } else {
155
                frame = ID3Frames.GENERIC_URL.create(specName, rawObject[specName], 3)
156
            }
157
        }
158
159
        if (frame && frame instanceof Buffer) {
160
            frames.push(frame)
161
        }
162
    })
163
164
    return frames
165
}
166
167
function readSync(filebuffer, options) {
168
    if(isString(filebuffer)) {
169
        filebuffer = fs.readFileSync(filebuffer)
170
    }
171
    return this.getTagsFromBuffer(filebuffer, options)
172
}
173
174
function readAsync(filebuffer, options, fn) {
175
    if(isString(filebuffer)) {
176
        fs.readFile(filebuffer, (error, data) => {
177
            if(error) {
178
                fn(error, null)
179
            } else {
180
                fn(null, this.getTagsFromBuffer(data, options))
181
            }
182
        })
183
    } else {
184
        fn(null, this.getTagsFromBuffer(filebuffer, options))
185
    }
186
}
187
188
/**
189
 * Read ID3-Tags from passed buffer/filepath
190
 * @param filebuffer - Can contain a filepath string or buffer
191
 * @param options - (optional) Object containing options
192
 * @param fn - (optional) Function for async version
193
 * @returns {boolean}
194
 */
195
module.exports.read = function(filebuffer, options, fn) {
196
    if(!options || typeof options === 'function') {
197
        fn = fn || options
198
        options = {}
199
    }
200
    if(isFunction(fn)) {
201
        return readAsync.bind(this)(filebuffer, options, fn)
202
    }
203
    return readSync.bind(this)(filebuffer, options)
204
}
205
206
/**
207
 * Update ID3-Tags from passed buffer/filepath
208
 * @param tags - Object containing tags to be written
209
 * @param filebuffer - Can contain a filepath string or buffer
210
 * @param options - (optional) Object containing options
211
 * @param fn - (optional) Function for async version
212
 * @returns {boolean|Buffer|Error}
213
 */
214
module.exports.update = function(tags, filebuffer, options, fn) {
215
    if(!options || typeof options === 'function') {
216
        fn = fn || options
217
        options = {}
218
    }
219
220
    const rawTags = Object.keys(tags).reduce((acc, val) => {
221
        if(ID3Definitions.FRAME_IDENTIFIERS.v3[val] !== undefined) {
222
            acc[ID3Definitions.FRAME_IDENTIFIERS.v3[val]] = tags[val]
223
        } else {
224
            acc[val] = tags[val]
225
        }
226
        return acc
227
    }, {})
228
229
    const updateFn = (currentTags) => {
230
        currentTags = currentTags.raw || {}
231
        Object.keys(rawTags).map((specName) => {
232
            const options = ID3Util.getSpecOptions(specName, 3)
233
            const cCompare = {}
234
            if(options.multiple && currentTags[specName] && rawTags[specName]) {
235
                if(options.updateCompareKey) {
236
                    currentTags[specName].forEach((cTag, index) => {
237
                        cCompare[cTag[options.updateCompareKey]] = index
238
                    })
239
240
                }
241
                if (!(rawTags[specName] instanceof Array)) {
242
                    rawTags[specName] = [rawTags[specName]]
243
                }
244
                rawTags[specName].forEach((rTag) => {
245
                    const comparison = cCompare[rTag[options.updateCompareKey]]
246
                    if (comparison !== undefined) {
247
                        currentTags[specName][comparison] = rTag
248
                    } else {
249
                        currentTags[specName].push(rTag)
250
                    }
251
                })
252
            } else {
253
                currentTags[specName] = rawTags[specName]
254
            }
255
        })
256
257
        return currentTags
258
    }
259
260
    if(!isFunction(fn)) {
261
        return this.write(updateFn(this.read(filebuffer, options)), filebuffer)
262
    }
263
264
    this.write(updateFn(this.read(filebuffer, options)), filebuffer, fn)
265
}
0 ignored issues
show
Best Practice introduced by
There is no return statement in this branch, but you do return something in other branches. Did you maybe miss it? If you do not want to return anything, consider adding return undefined; explicitly.
Loading history...
266
267
module.exports.getTagsFromBuffer = function(filebuffer, options) {
268
    let framePosition = ID3Util.getFramePosition(filebuffer)
269
    if(framePosition === -1) {
270
        return this.getTagsFromFrames([], 3, options)
271
    }
272
    const frameSize = ID3Util.decodeSize(filebuffer.slice(framePosition + 6, framePosition + 10)) + 10
273
    let ID3Frame = Buffer.alloc(frameSize + 1)
274
    filebuffer.copy(ID3Frame, 0, framePosition)
275
    //ID3 version e.g. 3 if ID3v2.3.0
276
    let ID3Version = ID3Frame[3]
277
    const tagFlags = ID3Util.parseTagHeaderFlags(ID3Frame)
278
    let extendedHeaderOffset = 0
279
    if(tagFlags.extendedHeader) {
280
        if(ID3Version === 3) {
281
            extendedHeaderOffset = 4 + filebuffer.readUInt32BE(10)
282
        } else if(ID3Version === 4) {
283
            extendedHeaderOffset = ID3Util.decodeSize(filebuffer.slice(10, 14))
284
        }
285
    }
286
    let ID3FrameBody = Buffer.alloc(frameSize - 10 - extendedHeaderOffset)
287
    filebuffer.copy(ID3FrameBody, 0, framePosition + 10 + extendedHeaderOffset)
288
289
    let frames = this.getFramesFromID3Body(ID3FrameBody, ID3Version, options)
290
291
    return this.getTagsFromFrames(frames, ID3Version, options)
292
}
293
294
module.exports.getFramesFromID3Body = function(ID3FrameBody, ID3Version, options = {}) {
295
    let currentPosition = 0
296
    let frames = []
297
    if(!ID3FrameBody || !(ID3FrameBody instanceof Buffer)) {
298
        return frames
299
    }
300
301
    let identifierSize = 4
302
    let textframeHeaderSize = 10
303
    if(ID3Version === 2) {
304
        identifierSize = 3
305
        textframeHeaderSize = 6
306
    }
307
308
    while(currentPosition < ID3FrameBody.length && ID3FrameBody[currentPosition] !== 0x00) {
309
        let bodyFrameHeader = Buffer.alloc(textframeHeaderSize)
310
        ID3FrameBody.copy(bodyFrameHeader, 0, currentPosition)
311
312
        let decodeSize = false
313
        if(ID3Version === 4) {
314
            decodeSize = true
315
        }
316
        let bodyFrameSize = ID3Util.getFrameSize(bodyFrameHeader, decodeSize, ID3Version)
317
        if(bodyFrameSize + 10 > (ID3FrameBody.length - currentPosition)) {
318
            break
319
        }
320
        const specName = bodyFrameHeader.toString('utf8', 0, identifierSize)
321
        if(options.exclude instanceof Array && options.exclude.includes(specName) || options.include instanceof Array && !options.include.includes(specName)) {
322
            currentPosition += bodyFrameSize + textframeHeaderSize
323
            continue
324
        }
325
        const frameHeaderFlags = ID3Util.parseFrameHeaderFlags(bodyFrameHeader, ID3Version)
326
        let bodyFrameBuffer = Buffer.alloc(bodyFrameSize)
327
        ID3FrameBody.copy(bodyFrameBuffer, 0, currentPosition + textframeHeaderSize + (frameHeaderFlags.dataLengthIndicator ? 4 : 0))
328
        //  Size of sub frame + its header
329
        currentPosition += bodyFrameSize + textframeHeaderSize
330
        frames.push({
331
            name: specName,
332
            flags: frameHeaderFlags,
333
            body: frameHeaderFlags.unsynchronisation ? ID3Util.processUnsynchronisedBuffer(bodyFrameBuffer) : bodyFrameBuffer
334
        })
335
    }
336
337
    return frames
338
}
339
340
module.exports.getTagsFromFrames = function(frames, ID3Version, options = {}) {
341
    let tags = { }
342
    let raw = { }
343
344
    frames.forEach((frame) => {
345
        const specName = ID3Version === 2 ? ID3Definitions.FRAME_IDENTIFIERS.v3[ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name]] : frame.name
346
        const identifier = ID3Version === 2 ? ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v2[frame.name] : ID3Definitions.FRAME_INTERNAL_IDENTIFIERS.v3[frame.name]
347
348
        if(!specName || !identifier || frame.flags.encryption) {
349
            return
350
        }
351
352
        if(frame.flags.compression) {
353
            if(frame.body.length < 5) {
354
                return
355
            }
356
            const inflatedSize = frame.body.readInt32BE()
357
            /*
358
            * ID3 spec defines that compression is stored in ZLIB format, but doesn't specify if header is present or not.
359
            * ZLIB has a 2-byte header.
360
            * 1. try if header + body decompression
361
            * 2. else try if header is not stored (assume that all content is deflated "body")
362
            * 3. else try if inflation works if the header is omitted (implementation dependent)
363
            * */
364
            try {
365
                frame.body = zlib.inflateSync(frame.body.slice(4))
366
            } catch (e) {
367
                try {
368
                    frame.body = zlib.inflateRawSync(frame.body.slice(4))
369
                } catch (e) {
370
                    try {
371
                        frame.body = zlib.inflateRawSync(frame.body.slice(6))
372
                    } catch (e) {
373
                        return
374
                    }
375
                }
376
            }
377
            if(frame.body.length !== inflatedSize) {
378
                return
379
            }
380
        }
381
382
        let decoded
383
        if(ID3Frames[specName]) {
384
            decoded = ID3Frames[specName].read(frame.body, ID3Version, this)
385
        } else if(specName.startsWith('T')) {
386
            decoded = ID3Frames.GENERIC_TEXT.read(frame.body, ID3Version)
387
        } else if(specName.startsWith('W')) {
388
            decoded = ID3Frames.GENERIC_URL.read(frame.body, ID3Version)
389
        }
390
391
        if(decoded) {
392
            if(ID3Util.getSpecOptions(specName, ID3Version).multiple) {
393
                if(!options.onlyRaw) {
394
                    if(!tags[identifier]) {
395
                        tags[identifier] = []
396
                    }
397
                    tags[identifier].push(decoded)
398
                }
399
                if(!options.noRaw) {
400
                    if(!raw[specName]) {
401
                        raw[specName] = []
402
                    }
403
                    raw[specName].push(decoded)
404
                }
405
            } else {
406
                if(!options.onlyRaw) {
407
                    tags[identifier] = decoded
408
                }
409
                if(!options.noRaw) {
410
                    raw[specName] = decoded
411
                }
412
            }
413
        }
414
    })
415
416
    if(options.onlyRaw) {
417
        return raw
418
    }
419
    if(options.noRaw) {
420
        return tags
421
    }
422
423
    tags.raw = raw
424
    return tags
425
}
426
427
/**
428
 * Checks and removes already written ID3-Frames from a buffer
429
 * @param data - Buffer
430
 * @returns {boolean|Buffer}
431
 */
432
module.exports.removeTagsFromBuffer = removeTagsFromBuffer
433
function removeTagsFromBuffer(data) {
434
        let framePosition = ID3Util.getFramePosition(data)
435
436
    if (framePosition === -1) {
437
        return data
438
    }
439
440
    let hSize = Buffer.from([data[framePosition + 6], data[framePosition + 7], data[framePosition + 8], data[framePosition + 9]])
441
442
    const isMsbSet = !!((hSize[0] | hSize[1] | hSize[2] | hSize[3]) & 0x80)
443
    if (isMsbSet) {
444
        //  Invalid tag size (msb not 0)
445
        return false
446
    }
447
448
    if (data.length >= framePosition + 10) {
449
        const size = ID3Util.decodeSize(data.slice(framePosition + 6, framePosition + 10))
450
        return Buffer.concat([data.slice(0, framePosition), data.slice(framePosition + size + 10)])
451
    }
452
453
    return data
454
}
455
456
/**
457
 * @param {string} filepath - Filepath to file
458
 * @returns {boolean|Error}
459
 */
460
function removeTagsSync(filepath) {
461
    let data
462
    try {
463
        data = fs.readFileSync(filepath)
464
    } catch(error) {
465
        return error
466
    }
467
468
    const newData = removeTagsFromBuffer(data)
469
    if(!newData) {
470
        return false
471
    }
472
473
    try {
474
        fs.writeFileSync(filepath, newData, 'binary')
475
    } catch(error) {
476
        return error
477
    }
478
479
    return true
480
}
481
482
/**
483
 * @param {string} filepath - Filepath to file
484
 * @param {(error: Error) => void} fn - Function for async usage
485
 * @returns {void}
486
 */
487
function removeTagsAsync(filepath, fn) {
488
    fs.readFile(filepath, (error, data) => {
489
        if(error) {
490
            fn(error)
491
        }
492
493
        const newData = removeTagsFromBuffer(data)
494
        if(!newData) {
495
            fn(error)
496
            return
497
        }
498
499
        fs.writeFile(filepath, newData, 'binary', (error) => {
500
            if(error) {
501
                fn(error)
502
            } else {
503
                fn(false)
504
            }
505
        })
506
    })
507
}
508
509
/**
510
 * Checks and removes already written ID3-Frames from a file
511
 * @param {string} filepath - Filepath to file
512
 * @param fn - (optional) Function for async usage
513
 * @returns {boolean|Error}
514
 */
515
module.exports.removeTags = function(filepath, fn) {
516
    if(isFunction(fn)) {
517
        return removeTagsAsync(filepath, fn)
518
    }
519
    return removeTagsSync(filepath)
520
}
521
522
function makePromise(fn) {
523
    return new Promise((resolve, reject) => {
524
        fn((error, result) => {
525
            if(error) {
526
                reject(error)
527
            } else {
528
                resolve(result)
529
            }
530
        })
531
    })
532
}
533
534
module.exports.Promise = {
535
    write: (tags, file) => makePromise(this.write.bind(this, tags, file)),
536
    update: (tags, file) => makePromise(this.update.bind(this, tags, file)),
537
    create: (tags) => {
538
        return new Promise((resolve) => {
539
            this.create(tags, (buffer) => {
540
                resolve(buffer)
541
            })
542
        })
543
    },
544
    read: (file, options) => makePromise(this.read.bind(this, file, options)),
545
    removeTags: (filepath) => makePromise(this.removeTags.bind(this, filepath))
546
}
547
548
module.exports.Constants = ID3Definitions.Constants
549